# Merise python parser and models generators.
#
# @autor: Patrice Ferlet <metal3d@gmail.com>
# @version 0.0.2
# @licence Gnu/GPL v3

# $Id: parser.py 56 2010-12-19 15:19:53Z Metal3d $
# $HeadURL: http://merisier.googlecode.com/svn/tags/0.0.2-beta1/src/merise/parser.py $

"""
Parser is used to read merise definition file
and fetch data definitions, relations and types

merise files defined datas without relation fields at first, then you can
find relations. Look at this example:

[customer]
+id
first_name
last_name
birthdate date not null

[products]
+id
name
price float(10.2)
description

[bill]
+id
bill_date date not null
payed int(1) default 0

#define relations
# table1 relation_to_table2 description backward_relation table2 (some attributes)
#where relation_to_table2 and backward_relation can be:
#0,n - 1,n - 1,1 - 0,1
#attributes set some additionnal fields...

#look example

#customer can pay 0 or X bill
#bill must have only one customer:

customer 0,n payement 1,1 bill

#a bill is composed by products (with quantity)
#bill have at least one product
#a product can be in no bill...

bill 1,n bill_composition 0,n products (quantity int default 1)

That's all !
"""
import os
import re

class Parser:
	"""
	Class that parse merise file
	"""
	def __init__(self, options):
		"""
		Initialize Parser with options (given from command line, see merise.py file)
		"""
		self.defs = {}
		self.links = []
		self.arrows = []
		self.keys = {}
		self.types = {}
		self.virtuals = []
		self.options = options
		
	def parse(self):
		"""
		Parse file to find entities and relations
		"""
		current = ''
		f = open(self.options.filename)

		for line in f:
			#remove comments, new lines etc...
			line=line.replace(",\n","")
			line=line.replace("\n","")
			line = re.sub(r'^#.*','',line)
			line=line.lower()
			
			#check if we are working on entity or relations
			d = re.findall(r'\[(.*)\]', line)
			d2 = re.findall(r'([^ ]*)\s+([0-9n,]*)\s+([^ ]*)\s+([0-9n,]*)\s+([^ \(]*)\s*(\(.*\))*$', line)
			
			#entity
			if len(d) > 0:
				dn = d[0]
				if re.findall(r'^\*', dn):
					#table have not to be written in sql
					dn = dn.replace('*','');
					self.virtuals.append(dn);
				self.defs[dn] = []
				current = dn
				self.keys[current]=[]
			else :
				#fields
				if len(d2) == 0:
					parts = line.split(' ')
					_attr = parts.pop(0)
					_type = " ".join(parts)
					
					#it's a key
					if re.search('\+', _attr):
						self.keys[current].append(_attr.replace('+','')) 
						try: 
							len(self.types[current])
						except:
							self.types[current] = []
						#_attr = _attr.replace('+','')
						if len(_type.strip())==0:
							_type = "auto"
						#types is defined as tuple: (field, type, primary key, relation table, is null)
						self.types[current].append( (_attr.replace('+','') , _type, False, None,False) )
					else:
						self.types[current].append( (_attr , _type, False, None, True) )
					self.defs[current].append(_attr)
				#relations
				else:
					self.links.append(d2[0])

	def getId(self,table,id=0):
		"""
		Check if key completion must be id_table or table_id (see options)
		"""
		if self.options.idpos == 'right':
			return table+'_'+self.keys[table][id]
		return self.keys[table][id]+'_'+table

	def findCardinals (self):
		"""
		Find cardinalities table x,x relation x,x table2 (attributes if possible)
		Example:
			Bill 1,n compose 0,n Products (quantity int default 1)
			You must read:
			Bill is composed by 1 or n Products with quantity
			Products can be in 0 or n Bill
		"""
		for rel in self.links:
			(table1, num1, link, num2 , table2, attr) = rel
			
			#card is X,1 X,n
			if re.search(',1', num1) and  (re.search(',n', num2) or re.search(',1', num2)):
				#find if table1 reference could be null,
				null = False
				if re.search('0,', num1):
					null = True
				i=0
				for k in self.keys[table2]:
					self.defs[table1].append(self.getId(table2,i))
					for (f,t,i,i2,nullity) in self.types[table2]:
						if k==f:
							_type = t
							
					self.types[table1].append((self.getId(table2,i), _type,True, table2,null))
					self.arrows.append (( (table2, self.getId(table2,i)) , (table1,  table1+"_"+self.getId(table2,i))))
					i+=1
									
			if re.search(',n', num1) and  re.search(',1', num2):
				#find if table2 reference could be null,
				null = False
				if re.search('0,', num2):
					null = True
				
				i=0
				for k in self.keys[table1]:
					self.defs[table2].append(self.getId(table1,i))
					i+=1
				j=0
				for k in self.keys[table1]:					
					for (f,t,i,i2,nullity) in self.types[table1]:
						if k==f:
							_type = t
					self.types[table2].append((self.getId(table1,j), _type,True, table1,null))
					#self.arrows.append ((self.keys[table2]+"_"+table2 , table2+"_"+"id_"+table1))
					self.arrows.append (( (table1, self.getId(table1,j)) , (table2,  table2+"_"+self.getId(table1,j))))
					j+=1
				
			#card X,n X,n
			if re.search(',n', num1) and  re.search(',n', num2):
				#create a cardinal table
				cardinal = link
				
				try:
					if self.defs[cardinal] is None:
						self.defs[cardinal] = []
						self.types[cardinal] = []
				except:
					self.defs[cardinal] = []
					self.types[cardinal] = []
				
				j=0
				for k in self.keys[table1]:
					if  k not in self.defs[cardinal]:
						null = False
						if re.search('0,', num1):
							null = True
						#TODO for now we force not null
						null = False
						self.defs[cardinal].append(self.getId(table1,j))
						for (f,t,i,i2,nullity) in self.types[table1]:
							if k==f:
								_type = t
								
						self.types[cardinal].append((self.getId(table1,j), _type, True, table1,null))
						self.arrows.append( ((cardinal, cardinal+"_"+self.getId(table1,j) )  ,  (table1, self.getId(table1,j) )) )
					j+=1
				j=0
				for k in self.keys[table2]:	
					if  k not in self.defs[cardinal]:
						null = False
						if re.search('0,', num2):
							null = True
						#TODO for now we force not null
						null = False
						self.defs[cardinal].append(self.getId(table2,j))
						for (f,t,i,i2,nullity) in self.types[table2]:
							if f and k==f:
								_type = t
						self.types[cardinal].append((self.getId(table2,j), _type, True, table2,null))
						self.arrows.append( ((cardinal, cardinal+"_"+self.getId(table2,j) )  ,  (table2, self.getId(table2,j) )) )
					j+=1
						
				if len(attr.strip()):
					attr = re.findall(r'\((.*)\)', attr)
					attr = attr[0].split(',')
					for a in attr:
						parts = a.strip().split(' ');
						_attr = parts.pop(0)
						self.defs[cardinal].append(_attr)
						if len(parts):
							_type = " ".join(parts)
						else:
							_type = "varchar(255)"
						self.types[cardinal].append((_attr, _type, False, None,None))
				
	def writeDataDict(self):
		for k in self.virtuals:
			try:
				del self.defs[k]
			except:
				pass
		
		(basename, ext) = os.path.splitext(os.path.basename(self.options.filename))
		path = os.path.join( self.options.directory, basename+".ini")
		ini = ""
		first = True
		keys = []
		for d in self.defs:
			if not first:
				ini += "\n"
			first = False
			ini += "[%s]\n" % (d)
			k = None
			try:
				i=0
				for kk in self.keys[d]:
				    #there is something wrong... with key names... TODO: correct this...
					#print "kk =", d, kk , i                	
					#k = self.getId(d,i)
					keys.append(kk)
					#print k
					ini += "id[]=%s\n" % (kk)
					i+=1
			except:
				pass
			for t in self.types[d]:
				if t[0] not in keys and len(t[0]) > 0:
					ini += "fields[]=%s\n" %str(t[0])
			
		f = open(path,"w")
		f.write(ini)
		f.close()
		print "%s generated" % (path)
